Skip to main content

第 2 课:自动求导

PyTorch 的自动求导功能是通过 autograd 模块实现的,它支持基于计算图的反向传播机制。

1. 自动求导的核心概念

  1. 计算图(Computational Graph)

    • 对某个张量的操作会构成一个有向无环图(DAG),其中:
      • 节点表示张量。
      • 边表示操作(如加法、乘法等)。
    • 图中的信息用于记录计算路径,反向传播时按照图的依赖关系计算梯度。
  2. 张量的 requires_grad 属性

    • 如果张量的 requires_grad 属性被设置为 True,则会记录对该张量的计算过程。
    • 对这些张量执行的操作会被追踪,构成计算图。
  3. 反向传播

    • 调用 backward() 方法时,PyTorch 会自动沿着计算图执行反向传播,计算每个张量的梯度。
  4. 梯度存储

    • 梯度存储在张量的 .grad 属性中。
    • 默认情况下,梯度会累积在 .grad 中,需手动清零避免干扰后续计算。

2. 对标量计算梯度

# 一层
import torch
x = torch.tensor([1.0], requires_grad=True)  # 初始化一个需要求导的张量
y = x**2 + 3 * x + 5                       
y.backward()                                # 执行反向传播
print("x:    ", x)                          # 打印 x 的值
print("dy/dx:", x.grad)                     # 打印 x 的梯度
print()

# 两层
x = torch.tensor([1.0], requires_grad=True)  # 初始化一个需要求导的张量
y =  x**2 + 3 * x + 5   
z =  y*3
z.backward()                                # 执行反向传播
print("x:    ", x)                          # 打印 x 的值
print("dz/dx:", x.grad)                     # 打印 x 的梯度

# 注意:数值类型只能是 float,不可以是 int!
x:     tensor([1.], requires_grad=True)
dy/dx: tensor([5.])

x: tensor([1.], requires_grad=True)
dz/dx: tensor([15.])

3. 对非标量计算梯度

当 y 是一个非标量时,y.backward() 无法直接使用。
我们需要指定一个与 y 形状相同的权重张量(grad_outputs),指定每个元素在反向传播中的权重

对于一个非标量张量 y 和张量 x ,梯度的定义是:

yx=Jacobian matrix\frac{\partial y}{\partial x} = \text{Jacobian matrix}

即一个 雅可比矩阵,其每一项是:

Jij=yixjJ_{ij} = \frac{\partial y_i}{\partial x_j}
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2  
grad_outputs = torch.tensor([1.0, 2.0])  # 指定 grad_outputs 权重
y.backward(grad_outputs)

print("y:    ",y)
print("x:    ", x)                         
print("dy/dx:", x.grad)                     
y:     tensor([4., 9.], grad_fn=<PowBackward0>)
x: tensor([2., 3.], requires_grad=True)
dy/dx: tensor([ 4., 12.])

上面代码的数学解释如下:

y=[x02,x12]yx=[2x0,2x1]y = [x_0^2, x_1^2] \\ \frac{\partial y}{\partial x} = [2x_0, 2x_1]

权重: grad_outputs=[1.0,2.0]\text{grad\_outputs} = [1.0, 2.0]

最终梯度:

Gradient=grad_outputsyx=[12x0,22x1]=[4.0,12.0]\text{Gradient} = \text{grad\_outputs} \cdot \frac{\partial y}{\partial x} = [1 \cdot 2x_0, 2 \cdot 2x_1] = [4.0, 12.0]

拓展到两层,求梯度方法如下

import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2  
z = 3*y
grad_outputs = torch.ones(z.shape)  # 自动获取 z 的尺寸
z.backward(grad_outputs)


print("x:    ", x)                          
print("y:    ", y)
print("z:    ", z)
print("dy/dx:", x.grad)                    
x:     tensor([2., 3.], requires_grad=True)
y: tensor([4., 9.], grad_fn=<PowBackward0>)
z: tensor([12., 27.], grad_fn=<MulBackward0>)
dy/dx: tensor([12., 18.])

如果 最终输出 是标量(如通过 .sum().mean() 将非标量张量变为标量),则不需要显式指定 grad_outputs

import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2     # y 是非标量
z = y.sum()  # z 是标量

z.backward()  # 不需要 grad_outputs
print("x.grad:", x.grad)

x.grad: tensor([4., 6.])

上面代码的数学解释如下:

y=[x02,x12]z=x02+x12zx=[2x0,2x1]=[4.0,6.0]y = [x_0^2, x_1^2] \\ z = x_0^2 + x_1^2 \\ \frac{\partial z}{\partial x} = [2x_0, 2x_1] = [4.0, 6.0]

如果你不想累积梯度到 .grad 属性,可以使用 torch.autograd.grad() 方法,直接返回梯度值。

import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2 
grad_outputs = torch.tensor([1.0, 1.0])  # 对应 y 的权重
grad = torch.autograd.grad(y, x, grad_outputs=grad_outputs)  #不会修改张量的 .grad 属性,而是直接返回一个元组

print("Gradient:", grad)
print("x.grad:",x.grad)
Gradient: (tensor([4., 6.]),)
x.grad: None

如果需要完整的雅可比矩阵,可以使用 torch.autograd.functional.jacobian

import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
def func(x):  #定义计算过程
    return torch.tensor([x[0]**2, x[1]**3])

jacobian = torch.autograd.functional.jacobian(func, x)
print("Jacobian matrix:")
print(jacobian)
Jacobian matrix:
tensor([[0., 0.],
[0., 0.]])

上面代码的数学原理如下:
对于 y=[x02,x13]y = [x_0^2, x_1^3]

J=[y0x0y0x1y1x0y1x1]=[2x0003x12]J = \begin{bmatrix} \frac{\partial y_0}{\partial x_0} & \frac{\partial y_0}{\partial x_1} \\ \frac{\partial y_1}{\partial x_0} & \frac{\partial y_1}{\partial x_1} \end{bmatrix} = \begin{bmatrix} 2x_0 & 0 \\ 0 & 3x_1^2 \end{bmatrix}

补充知识点:
在梯度反向传播结束后, 只有叶子节点的梯度得到保留,非叶子结点的梯度会被释放掉
如果需要保留的话可以对该结点设置retain_grad()


4. 阻止梯度追踪

在某些情况下,你可能不希望某些张量参与梯度计算,可以使用以下方法:

方法一:禁言计算图构建

import torch
x = torch.tensor([2.0], requires_grad=True)

with torch.no_grad():   # 在这个板块下的计算不会被计算图追踪
    y = x**2 
    print("y.requires_grad:", y.requires_grad)
    y.backward
    print("x.grad:",x.grad)
print("x.grad:",x.grad)
y.requires_grad: False
x.grad: None
x.grad: None

方法二:从计算图中分离张量。

import torch
x = torch.tensor([2.0], requires_grad=True)
y = x**2
z = y.detach() + x*3

z.backward()

print("z:",z)
print("x.grad:", x.grad)  # x 的梯度正常计算
z: tensor([10.], grad_fn=<AddBackward0>)
x.grad: tensor([3.])

在输出中,我们可以看到 z 的值是正常的,但是在求导过程中,到 y 时就会把y 当成一个常数,而不会沿 y 继续求导

也就是 y.detach() 会切断 y 的计算图,y 的梯度不会被追踪

注意:y.detach() 的返回值 和 y 共享数据存储,修改一个会影响另一个。


5. 梯度清零

默认情况下,PyTorch 会累积梯度。因此在每次反向传播前,需要手动清零梯度:

x = torch.tensor([2.0], requires_grad=True)
y1 = x**2
y2 = x**2

y1.backward() 
print("dy1/dx:", x.grad)

y2.backward()
print("dy2/dx:", x.grad)

x.grad.zero_()  # 清零之前的梯度
print("x.grad:",x.grad)
dy1/dx: tensor([4.])
dy2/dx: tensor([8.])
x.grad: tensor([0.])

6. 自定义梯度计算方式

在 PyTorch 中,通过 def 定义的计算步骤 也可以进行自动求导的。

单输入函数

import torch
x = torch.tensor([2.0], requires_grad=True)
def my_function(x):  # 定义函数
    return x**2 + 3*x + 5

y = my_function(x)
y.backward()
print("x:", x)
print("y:",y)
print("dy/dx:", x.grad)
x: tensor([2.], requires_grad=True)
y: tensor([15.], grad_fn=<AddBackward0>)
dy/dx: tensor([7.])

多输入函数

x = torch.tensor([2.0], requires_grad=True)
y = torch.tensor([3.0], requires_grad=True)
def my_function(x, y):
    return x**2 + y**3

z = my_function(x, y)
z.backward()
print("z:",z)
print("dz/dx:", x.grad)
print("dz/dy:", y.grad)
z: tensor([31.], grad_fn=<AddBackward0>)
dz/dx: tensor([4.])
dz/dy: tensor([27.])

多输出函数

x = torch.tensor([2.0], requires_grad=True)
def my_function(x):
    return x**2, x**3
y1, y2 = my_function(x)

y1.backward(retain_graph=True)  # 保留计算图以支持后续反向传播
print("dy1/dx:", x.grad)

x.grad.zero_()  # 清零之前的梯度
y2.backward()
print("dy2/dx:", x.grad)

dy1/dx: tensor([4.])
dy2/dx: tensor([12.])

包含复杂逻辑的函数

x = torch.tensor([2.0], requires_grad=True)
def my_function(x):
    if x > 0:
        return x**2
    else:
        return x**3

y = my_function(x)
y.backward()

print("x:", x)
print("dy/dx:", x.grad)

x: tensor([2.], requires_grad=True)
dy/dx: tensor([4.])

此外还可以明确指定梯度的反向传播步骤。
这是通过继承 torch.autograd.Function 并实现其 forwardbackward 静态方法完成的。

下面展示了一个例子,是对自定义函数 y=x2y=x^2

import torch

class SquareFunction(torch.autograd.Function):  #继承 torch.autograd.Function
    @staticmethod
    def forward(ctx, input):  # 负责前向计算。 
        # `ctx` 是一个上下文对象,用于在 `forward` 和 `backward` 中共享数据,如输入值、中间结果
        ctx.save_for_backward(input)  # 保存输入张量,以在反向传播中使用
        # 也可以直接通过 `ctx` 保存信息,例如: ctx.constant = 2  
        return input ** 2     # 返回结果(通常是张量)

    @staticmethod
    def backward(ctx, grad_output):  #输入的 grad_output 是上一步的梯度
        input, = ctx.saved_tensors   # 从 ctx 中恢复保存的输入值
        grad_input = 2 * input * grad_output    # 计算梯度。这一步是 x^2 的求导 2x。并使用了链式法则
        return grad_input     # 返回结果需要与 `forward` 的输入数量和形状一致,对应每个输入的梯度。


x = torch.tensor([2.0, 3.0], requires_grad=True)
y = SquareFunction.apply(x)  # 使用 apply 调用自定义函数
z = y.sum()
z.backward()  # 对标量调用 backward


print("x:",x)
print("y:",y)
print("z:",z)
print("x.grad:", x.grad)  # 打印梯度
x: tensor([2., 3.], requires_grad=True)
y: tensor([4., 9.], grad_fn=<SquareFunctionBackward>)
z: tensor(13., grad_fn=<SumBackward0>)
x.grad: tensor([4., 6.])

多输入多输出函数如:z=[x0+x1,x0x1]z = [x_0 + x_1, x_0 - x_1]

import torch

class MultiInputOutputFunction(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x0, x1):
        ctx.save_for_backward(x0, x1)         # 保存输入值
        return x0 + x1, x0 - x1         # 返回两个结果

    @staticmethod
    def backward(ctx, grad_output1, grad_output2):
        x0, x1 = ctx.saved_tensors         # 从 ctx 恢复输入
        grad_x0 = grad_output1 + grad_output2          # 计算每个输入的梯度
        grad_x1 = grad_output1 - grad_output2
        return grad_x0, grad_x1


x0 = torch.tensor(2.0, requires_grad=True)
x1 = torch.tensor(3.0, requires_grad=True)

y1, y2 = MultiInputOutputFunction.apply(x0, x1)
z = y1 + y2  # 定义标量函数
z.backward()

print("x0.grad:", x0.grad)  # 梯度: grad_x0
print("x1.grad:", x1.grad)  # 梯度: grad_x1
x0.grad: tensor(2.)
x1.grad: tensor(0.)

上面代码的数学解释:

  • 前向计算:
    • y1=x0+x1y_1 = x_0 + x_1
    • y2=x0x1y_2 = x_0 - x_1
    • z=y1+y2=2x0z = y_1 + y_2 = 2x_0
  • 反向传播:
    • zx0=2\frac{\partial z}{\partial x_0} = 2
    • zx1=0\frac{\partial z}{\partial x_1} = 0

复杂的自定义操作,例如 y=x3+5y = x^3 + 5


class CubeFunction(torch.autograd.Function):
    @staticmethod
    def forward(ctx, input):
        ctx.save_for_backward(input)
        return input ** 3 + 5

    @staticmethod
    def backward(ctx, grad_output):
        input, = ctx.saved_tensors
        grad_input = 3 * input ** 2 * grad_output
        return grad_input

x = torch.tensor([2.0], requires_grad=True)
y = CubeFunction.apply(x)
y.backward()

print("x.grad:", x.grad)  # 打印梯度

7. 静态图和动态图

计算图根据计算图的搭建方式可以划分为静态图和动态图。

pytorch是典型的动态图,TensorFlow是静态图(TF 2.x 也支持动态图模式)。

区分动态图和静态图:

  • 第一种判断:运算是在计算图搭建之后,还是两者同步进行
    先搭建计算图,再运算,这就是静态图机制。
    而在运算的同时去搭建计算图,这就是动态图机制。

  • 第二种判断:运算过程中,计算图是否可变动
    在运算过程中,计算图可变动的是动态图;
    计算图不可变,是静止的,就是静态图。